<?php

namespace liketp;

use Closure;
use liketp\event\DbSubscribe;
use liketp\exception\Exception;
use ReflectionClass;
use ReflectionMethod;
use Throwable;

class Event
{
    protected $app;
    protected $init = false;

    protected $subscribers = [
        DbSubscribe::class
    ];

    protected $bindList = [];
    protected $listenList = [];

    public function __construct(App $app)
    {
        $this->app = $app;
    }

    public function applyConfig(array $config)
    {
        $this->init || $this->init();
        isset($config['bind']) && $this->bind($config['bind']);
        isset($config['listen']) && $this->listenEvents($config['listen']);
        isset($config['subscribe']) && $this->subscribe($config['subscribe']);
    }

    protected function init()
    {
        if ($this->init) return;

        $this->init = true;
        $this->subscribe($this->subscribers);
    }

    public function bind($flag, $target = null)
    {
        if (is_array($flag)) {
            foreach ($flag as $k => $v) {
                $this->bind($k, $v);
            }
        } else if (is_string($flag)) {
            if (empty($target)) return;
            if (is_object($target)) $target = get_class($target);
            $this->bindList[$flag] = $target;
        }

        return $this;
    }

    public function listenEvents(array $listenList)
    {
        foreach ($listenList as $event => $handle) {
            if (is_array($handle)) {
                foreach ($handle as $h) {
                    $this->listen($event, $h);
                }
            } else {
                $this->listen($event, $handle);
            }
        }
        return $this;
    }

    public function listen(string $event, $handle)
    {
        if (empty($event)) return $this;

        $event = $this->getBind($event, $event);
        $handleList = $this->getListen($event);
        $handleList[] = $handle;

        $this->listenList[$event] = $handleList;

        return $this;
    }

    public function subscribe($subscriber)
    {
        if (is_array($subscriber)) {
            foreach ($subscriber as $sub) {
                $this->subscribe($sub);
            }
        } else {
            if (is_string($subscriber)) {
                $subscriber = $this->app->make($subscriber);
            }

            if (method_exists($subscriber, 'subscribe')) {
                $subscriber->subscribe();
                return $this;
            }

            $reflectClass = new ReflectionClass($subscriber);
            $eventPrefix = $this->getEventPrefix($reflectClass, $subscriber);
            $methods = $reflectClass->getMethods(ReflectionMethod::IS_PUBLIC);

            foreach ($methods as $method) {
                $name = $method->getName();
                if (substr($name, 0, 2) == 'on') {
                    $this->listen($eventPrefix . substr($name, 2), [$subscriber, $name]);
                }
            }
        }

        return $this;
    }

    public function getBind(?string $flag = null, $default = '')
    {
        if (is_null($flag)) return $this->bindList;
        return $this->bindList[$flag] ?? $default;
    }

    public function getListen($flag = null)
    {
        if (is_null($flag)) return $this->listenList;
        if (is_object($flag)) $flag = get_class($flag);

        return $this->listenList[$flag] ?? [];
    }

    public function trigger($event, $param = null, bool $returnLast = false)
    {
        if (empty($event)) return;

        if (is_object($event)) {
            $param = $event;
            $event = get_class($event);
        }

        $event = $this->getBind($event, $event);
        $handleList = $this->getListen($event);
        $handleList = array_unique($handleList, SORT_REGULAR);

        $result = [];
        foreach ($handleList as $k => $handle) {
            try {
                $res = $this->dispatch($handle, [$param]);
            } catch (Throwable $e) {

                $msg = $this->assemblyExceptionMsg($event, $handle, $e);
                throw new Exception($msg);
            }
            if ($res === false) break;
            $result[$k] = $res;
        }

        return $returnLast ? end($result) : $result;
    }


    //------------------------------------------------ protected
    protected function dispatch($handle, array $params = [])
    {
        // listen(flag, function(){})
        if ($handle instanceof Closure) {
            return $this->app->invokeFunction($handle, $params);
        }
        // listen(flag, [spacename/classname, method])
        else if (is_array($handle)) {
            return $this->app->invokeMethod($handle, $params);
        }
        // listen(flag, spaceName/className::method) | listen(flag, spaceName/className)
        else if (is_string($handle)) {
            if (strpos($handle, '::')) {
                $callable = $handle;
            } else {
                $callable = [$handle, 'handle'];
            }
            return $this->app->invokeMethod($callable, $params);
        }
    }

    protected function getEventPrefix($reflectClass, $obj)
    {
        $eventPrefix = '';
        if ($reflectClass->hasProperty('eventPrefix')) {
            $prefix = $reflectClass->getProperty('eventPrefix');
            $prefix->setAccessible(true);
            $eventPrefix = $prefix->getValue($obj);
        }
        return $eventPrefix;
    }

    protected function assemblyExceptionMsg($event, $handle, $e)
    {
        $callable = '';
        $event = is_object($event) ? get_class($event) : $event;

        if (is_array($handle)) {
            if (is_object($handle[0] ?? null)) {
                $callable = get_class($handle[0]) . ' -> ' . ($handle[1] ?? '');
            } else {
                $callable = json_encode($handle);
            }
        } else if (is_string($handle)) {
            $callable = $handle;
        } else if ($handle instanceof Closure) {
            $callable = 'Closure function';
        }

        $msg = 'Event(' . $event . ') handle trigger failed';
        $msg .= ', [' . $callable . ']';
        $msg .= ', msg: ' . $e->getMessage();

        return $msg;
    }
}
